"""
缺失值处理案例
演示各种缺失值检测和处理方法
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')

# 设置中文显示
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

print("=" * 80)
print("缺失值处理案例")
print("=" * 80)

# ============================================================================
# 步骤1：创建带缺失值的示例数据
# ============================================================================

print("\n" + "=" * 80)
print("步骤1：创建带缺失值的示例数据")
print("=" * 80)

# 设置随机种子
np.random.seed(42)

# 创建基础数据
n_samples = 1000
data = {
    'age': np.random.randint(18, 80, n_samples),
    'income': np.random.normal(50000, 20000, n_samples),
    'credit_score': np.random.randint(300, 850, n_samples),
    'years_employed': np.random.randint(0, 40, n_samples),
    'debt': np.random.normal(10000, 5000, n_samples),
    'education': np.random.choice(['高中', '本科', '硕士', '博士'], n_samples),
    'city': np.random.choice(['北京', '上海', '广州', '深圳'], n_samples)
}

df = pd.DataFrame(data)

# 人为引入缺失值（不同的缺失机制）
# 完全随机缺失 (MCAR)
missing_indices = np.random.choice(df.index, size=int(0.1 * n_samples), replace=False)
df.loc[missing_indices, 'age'] = np.nan

# 随机缺失 (MAR) - 年轻人更不愿透露收入
young_indices = df[df['age'] < 30].index
missing_income = np.random.choice(young_indices, size=int(0.3 * len(young_indices)), replace=False)
df.loc[missing_income, 'income'] = np.nan

# 非随机缺失 (MNAR) - 信用分数低的人不愿透露
low_credit = df[df['credit_score'] < 500].index
missing_credit = np.random.choice(low_credit, size=int(0.4 * len(low_credit)), replace=False)
df.loc[missing_credit, 'credit_score'] = np.nan

# 其他列随机缺失
df.loc[np.random.choice(df.index, 50, replace=False), 'years_employed'] = np.nan
df.loc[np.random.choice(df.index, 80, replace=False), 'education'] = np.nan

print(f"数据集形状: {df.shape}")
print(f"\n数据预览：")
print(df.head(10))

# ============================================================================
# 步骤2：缺失值检测与分析
# ============================================================================

print("\n" + "=" * 80)
print("步骤2：缺失值检测与分析")
print("=" * 80)

# 2.1 统计缺失值
print("\n2.1 缺失值统计")
print("-" * 80)

missing_stats = pd.DataFrame({
    '缺失数量': df.isnull().sum(),
    '缺失比例': df.isnull().mean(),
    '数据类型': df.dtypes
})
missing_stats = missing_stats[missing_stats['缺失数量'] > 0].sort_values('缺失比例', ascending=False)

print(missing_stats)

# 2.2 可视化缺失值模式
print("\n2.2 可视化缺失值模式")
print("-" * 80)

fig, axes = plt.subplots(2, 2, figsize=(16, 10))

# 缺失值条形图
missing_counts = df.isnull().sum()
missing_counts = missing_counts[missing_counts > 0].sort_values(ascending=False)
axes[0, 0].barh(missing_counts.index, missing_counts.values, color='coral')
axes[0, 0].set_xlabel('缺失数量')
axes[0, 0].set_title('各列缺失值数量', fontweight='bold')
axes[0, 0].grid(True, alpha=0.3, axis='x')

# 缺失值比例
missing_ratio = df.isnull().mean()
missing_ratio = missing_ratio[missing_ratio > 0].sort_values(ascending=False)
axes[0, 1].barh(missing_ratio.index, missing_ratio.values * 100, color='skyblue')
axes[0, 1].set_xlabel('缺失比例 (%)')
axes[0, 1].set_title('各列缺失值比例', fontweight='bold')
axes[0, 1].grid(True, alpha=0.3, axis='x')

# 缺失值热力图
missing_matrix = df.isnull().astype(int)
sns.heatmap(missing_matrix.iloc[:100], cmap='YlOrRd', cbar=True, 
            yticklabels=False, ax=axes[1, 0])
axes[1, 0].set_title('缺失值模式热力图（前100行）', fontweight='bold')
axes[1, 0].set_xlabel('列')

# 缺失值相关性
missing_corr = missing_matrix.corr()
sns.heatmap(missing_corr, annot=True, fmt='.2f', cmap='coolwarm', 
            center=0, ax=axes[1, 1], square=True)
axes[1, 1].set_title('缺失值相关性矩阵', fontweight='bold')

plt.tight_layout()
plt.savefig('/Users/binming/Desktop/CodeBase/python/机器学习/特征工程/0-数据清洗/缺失值分析.png',
            dpi=300, bbox_inches='tight')
print("缺失值分析图已保存")

# ============================================================================
# 步骤3：缺失值处理方法
# ============================================================================

print("\n" + "=" * 80)
print("步骤3：缺失值处理方法")
print("=" * 80)

# 创建数据副本用于不同方法
df_original = df.copy()

# 3.1 删除法
print("\n3.1 删除法")
print("-" * 80)

# 删除包含缺失值的行
df_drop_rows = df.dropna()
print(f"删除缺失行后: {df_drop_rows.shape} (损失 {len(df) - len(df_drop_rows)} 行)")

# 删除缺失值过多的列（阈值50%）
threshold = 0.5
df_drop_cols = df.dropna(thresh=int(threshold * len(df)), axis=1)
print(f"删除缺失列后: {df_drop_cols.shape}")

# 3.2 简单填充法
print("\n3.2 简单填充法")
print("-" * 80)

df_simple = df.copy()

# 数值型：均值填充
print("\n均值填充（数值型特征）:")
numeric_cols = df_simple.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
    if df_simple[col].isnull().any():
        mean_value = df_simple[col].mean()
        df_simple[col].fillna(mean_value, inplace=True)
        print(f"  {col}: 填充值 = {mean_value:.2f}")

# 类别型：众数填充
print("\n众数填充（类别型特征）:")
categorical_cols = df_simple.select_dtypes(include=['object']).columns
for col in categorical_cols:
    if df_simple[col].isnull().any():
        mode_value = df_simple[col].mode()[0]
        df_simple[col].fillna(mode_value, inplace=True)
        print(f"  {col}: 填充值 = {mode_value}")

print(f"\n填充后缺失值: {df_simple.isnull().sum().sum()}")

# 3.3 中位数填充（对异常值更鲁棒）
print("\n3.3 中位数填充")
print("-" * 80)

df_median = df.copy()
for col in numeric_cols:
    if df_median[col].isnull().any():
        median_value = df_median[col].median()
        df_median[col].fillna(median_value, inplace=True)
        print(f"{col}: 中位数 = {median_value:.2f}")

# 3.4 使用sklearn的SimpleImputer
print("\n3.4 使用sklearn的SimpleImputer")
print("-" * 80)

df_imputer = df.copy()

# 数值型特征
numeric_data = df_imputer[numeric_cols]
imputer_mean = SimpleImputer(strategy='mean')
imputed_numeric = imputer_mean.fit_transform(numeric_data)
df_imputer[numeric_cols] = imputed_numeric

# 类别型特征
categorical_data = df_imputer[categorical_cols]
imputer_mode = SimpleImputer(strategy='most_frequent')
imputed_categorical = imputer_mode.fit_transform(categorical_data)
df_imputer[categorical_cols] = imputed_categorical

print(f"填充后缺失值: {df_imputer.isnull().sum().sum()}")

# 3.5 前向/后向填充（适用于时间序列）
print("\n3.5 前向/后向填充")
print("-" * 80)

df_ffill = df.copy()
df_ffill_result = df_ffill.fillna(method='ffill')  # 前向填充
df_bfill_result = df_ffill.fillna(method='bfill')  # 后向填充

print(f"前向填充后缺失值: {df_ffill_result.isnull().sum().sum()}")
print(f"后向填充后缺失值: {df_bfill_result.isnull().sum().sum()}")

# 3.6 插值法
print("\n3.6 插值法")
print("-" * 80)

df_interpolate = df.copy()

# 线性插值
df_linear = df_interpolate[numeric_cols].interpolate(method='linear')
print(f"线性插值后缺失值: {df_linear.isnull().sum().sum()}")

# 多项式插值
df_poly = df_interpolate[numeric_cols].interpolate(method='polynomial', order=2)
print(f"多项式插值后缺失值: {df_poly.isnull().sum().sum()}")

# 3.7 KNN填充
print("\n3.7 KNN填充（K近邻填充）")
print("-" * 80)

df_knn = df.copy()
numeric_data = df_knn[numeric_cols]

knn_imputer = KNNImputer(n_neighbors=5)
imputed_knn = knn_imputer.fit_transform(numeric_data)
df_knn[numeric_cols] = imputed_knn

print(f"KNN填充后缺失值: {df_knn.isnull().sum().sum()}")
print("KNN填充使用5个最近邻的平均值")

# 3.8 迭代填充（MICE - 多重插补）
print("\n3.8 迭代填充（MICE - Multivariate Imputation by Chained Equations）")
print("-" * 80)

df_mice = df.copy()
numeric_data = df_mice[numeric_cols]

mice_imputer = IterativeImputer(
    estimator=RandomForestRegressor(n_estimators=10, random_state=42),
    max_iter=10,
    random_state=42
)
imputed_mice = mice_imputer.fit_transform(numeric_data)
df_mice[numeric_cols] = imputed_mice

print(f"MICE填充后缺失值: {df_mice.isnull().sum().sum()}")
print("MICE使用随机森林模型迭代预测缺失值")

# 3.9 缺失值标记
print("\n3.9 缺失值标记（创建指示变量）")
print("-" * 80)

df_indicator = df.copy()

# 为每个有缺失值的列创建指示变量
for col in df.columns:
    if df[col].isnull().any():
        df_indicator[f'{col}_missing'] = df[col].isnull().astype(int)

# 然后填充原始缺失值
df_indicator[numeric_cols] = df_indicator[numeric_cols].fillna(df_indicator[numeric_cols].mean())
df_indicator[categorical_cols] = df_indicator[categorical_cols].fillna(df_indicator[categorical_cols].mode().iloc[0])

print(f"新增指示变量: {[col for col in df_indicator.columns if '_missing' in col]}")
print(f"数据集形状: {df_indicator.shape}")

# ============================================================================
# 步骤4：对比不同填充方法的效果
# ============================================================================

print("\n" + "=" * 80)
print("步骤4：对比不同填充方法的效果")
print("=" * 80)

# 创建一个完整的数据集用于评估
df_complete = df.dropna()
print(f"完整数据集大小: {len(df_complete)}")

# 人为引入缺失值
df_test = df_complete.copy()
missing_mask = np.random.rand(len(df_test), len(numeric_cols)) < 0.2
for i, col in enumerate(numeric_cols):
    df_test.loc[missing_mask[:, i], col] = np.nan

print(f"引入缺失值后: {df_test.isnull().sum().sum()} 个缺失值")

# 测试不同填充方法
methods = {
    '均值填充': SimpleImputer(strategy='mean'),
    '中位数填充': SimpleImputer(strategy='median'),
    'KNN填充': KNNImputer(n_neighbors=5),
    'MICE填充': IterativeImputer(max_iter=10, random_state=42)
}

results = []

for method_name, imputer in methods.items():
    # 填充
    df_filled = df_test.copy()
    df_filled[numeric_cols] = imputer.fit_transform(df_test[numeric_cols])
    
    # 计算误差（与原始完整数据对比）
    mse_list = []
    for col in numeric_cols:
        mask = missing_mask[:, list(numeric_cols).index(col)]
        if mask.sum() > 0:
            mse = mean_squared_error(
                df_complete.loc[mask, col],
                df_filled.loc[mask, col]
            )
            mse_list.append(mse)
    
    avg_mse = np.mean(mse_list)
    results.append({
        '方法': method_name,
        '平均MSE': avg_mse,
        'RMSE': np.sqrt(avg_mse)
    })

results_df = pd.DataFrame(results).sort_values('平均MSE')
print("\n不同填充方法的误差对比：")
print(results_df.to_string(index=False))

# ============================================================================
# 步骤5：可视化对比
# ============================================================================

print("\n" + "=" * 80)
print("步骤5：可视化对比不同填充方法")
print("=" * 80)

fig, axes = plt.subplots(2, 3, figsize=(18, 10))
fig.suptitle('不同缺失值填充方法对比', fontsize=16, fontweight='bold')

# 选择一个特征进行可视化
feature = 'income'

# 原始数据分布
axes[0, 0].hist(df_original[feature].dropna(), bins=50, alpha=0.7, edgecolor='black')
axes[0, 0].set_title('原始数据分布', fontweight='bold')
axes[0, 0].set_xlabel(feature)
axes[0, 0].set_ylabel('频数')
axes[0, 0].grid(True, alpha=0.3)

# 均值填充
axes[0, 1].hist(df_simple[feature], bins=50, alpha=0.7, edgecolor='black', color='orange')
axes[0, 1].axvline(df_simple[feature].mean(), color='red', linestyle='--', 
                   label=f'均值: {df_simple[feature].mean():.0f}')
axes[0, 1].set_title('均值填充', fontweight='bold')
axes[0, 1].set_xlabel(feature)
axes[0, 1].set_ylabel('频数')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# 中位数填充
axes[0, 2].hist(df_median[feature], bins=50, alpha=0.7, edgecolor='black', color='green')
axes[0, 2].axvline(df_median[feature].median(), color='red', linestyle='--',
                   label=f'中位数: {df_median[feature].median():.0f}')
axes[0, 2].set_title('中位数填充', fontweight='bold')
axes[0, 2].set_xlabel(feature)
axes[0, 2].set_ylabel('频数')
axes[0, 2].legend()
axes[0, 2].grid(True, alpha=0.3)

# KNN填充
axes[1, 0].hist(df_knn[feature], bins=50, alpha=0.7, edgecolor='black', color='purple')
axes[1, 0].set_title('KNN填充', fontweight='bold')
axes[1, 0].set_xlabel(feature)
axes[1, 0].set_ylabel('频数')
axes[1, 0].grid(True, alpha=0.3)

# MICE填充
axes[1, 1].hist(df_mice[feature], bins=50, alpha=0.7, edgecolor='black', color='red')
axes[1, 1].set_title('MICE填充', fontweight='bold')
axes[1, 1].set_xlabel(feature)
axes[1, 1].set_ylabel('频数')
axes[1, 1].grid(True, alpha=0.3)

# 误差对比
axes[1, 2].barh(results_df['方法'], results_df['RMSE'], color='skyblue')
axes[1, 2].set_xlabel('RMSE')
axes[1, 2].set_title('填充方法误差对比', fontweight='bold')
axes[1, 2].grid(True, alpha=0.3, axis='x')

plt.tight_layout()
plt.savefig('/Users/binming/Desktop/CodeBase/python/机器学习/特征工程/0-数据清洗/填充方法对比.png',
            dpi=300, bbox_inches='tight')
print("填充方法对比图已保存")

# ============================================================================
# 步骤6：最佳实践建议
# ============================================================================

print("\n" + "=" * 80)
print("步骤6：缺失值处理最佳实践")
print("=" * 80)

print("""
┌─────────────────┬──────────────────────┬─────────────────────┬──────────────────┐
│   缺失比例      │      推荐方法        │        优点         │      缺点        │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ < 5%            │ 删除或简单填充       │ 简单快速            │ 可能损失信息     │
│                 │ (均值/中位数/众数)   │                     │                  │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 5% - 20%        │ KNN填充或插值        │ 考虑特征关系        │ 计算成本较高     │
│                 │                      │ 保留数据分布        │                  │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 20% - 50%       │ MICE或模型预测       │ 最准确              │ 计算成本高       │
│                 │ + 缺失标记           │ 保留缺失信息        │ 可能过拟合       │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ > 50%           │ 考虑删除该特征       │ 避免引入噪声        │ 损失特征         │
│                 │ 或收集更多数据       │                     │                  │
└─────────────────┴──────────────────────┴─────────────────────┴──────────────────┘

选择策略：

1. **数据类型**
   - 数值型：均值/中位数/KNN/MICE
   - 类别型：众数/KNN
   - 时间序列：前向/后向填充/插值

2. **缺失机制**
   - MCAR：任何方法都可以
   - MAR：KNN或MICE
   - MNAR：需要建模或领域知识

3. **模型类型**
   - 线性模型：简单填充即可
   - 树模型：可以保留缺失值或简单填充
   - 神经网络：需要仔细填充

4. **业务场景**
   - 探索性分析：简单填充
   - 生产环境：稳定的填充方法
   - 竞赛：尝试多种方法

关键要点：
✓ 先分析缺失模式，再选择方法
✓ 在训练集上fit，在测试集上transform
✓ 考虑创建缺失指示变量
✓ 记录填充策略，便于复现
✓ 评估填充效果对模型的影响
✓ 结合领域知识，不要盲目填充
""")

print("\n" + "=" * 80)
print("案例完成！")
print("=" * 80)
